/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.gallery3d.glrenderer;

import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.opengl.GLU;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.util.Log;

import com.android.gallery3d.common.Utils;
import com.android.gallery3d.util.IntArray;

import junit.framework.Assert;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11Ext;
import javax.microedition.khronos.opengles.GL11ExtensionPack;

public class GLES11Canvas implements GLCanvas {
	@SuppressWarnings("unused")
	private static final String TAG = "GLCanvasImp";

	private static final float OPAQUE_ALPHA = 0.95f;

	private static final int OFFSET_FILL_RECT = 0;
	private static final int OFFSET_DRAW_LINE = 4;
	private static final int OFFSET_DRAW_RECT = 6;
	private static final float[] BOX_COORDINATES = { 0, 0, 1, 0, 0, 1, 1, 1, // used
																				// for
																				// filling
																				// a
																				// rectangle
			0, 0, 1, 1, // used for drawing a line
			0, 0, 0, 1, 1, 1, 1, 0 }; // used for drawing the outline of a
										// rectangle

	private GL11 mGL;

	private final float mMatrixValues[] = new float[16];
	private final float mTextureMatrixValues[] = new float[16];

	// The results of mapPoints are stored in this buffer, and the order is
	// x1, y1, x2, y2.
	private final float mMapPointsBuffer[] = new float[4];

	private final float mTextureColor[] = new float[4];

	private int mBoxCoords;

	private GLState mGLState;
	private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>();

	private float mAlpha;
	private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
	private ConfigState mRecycledRestoreAction;

	private final RectF mDrawTextureSourceRect = new RectF();
	private final RectF mDrawTextureTargetRect = new RectF();
	private final float[] mTempMatrix = new float[32];
	private final IntArray mUnboundTextures = new IntArray();
	private final IntArray mDeleteBuffers = new IntArray();
	private int mScreenWidth;
	private int mScreenHeight;
	private boolean mBlendEnabled = true;
	private int mFrameBuffer[] = new int[1];
	private static float[] sCropRect = new float[4];

	private RawTexture mTargetTexture;

	// Drawing statistics
	int mCountDrawLine;
	int mCountFillRect;
	int mCountDrawMesh;
	int mCountTextureRect;
	int mCountTextureOES;

	private static GLId mGLId = new GLES11IdImpl();

	public GLES11Canvas(GL11 gl) {
		mGL = gl;
		mGLState = new GLState(gl);
		// First create an nio buffer, then create a VBO from it.
		int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE;
		FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size)
				.asFloatBuffer();
		xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);

		int[] name = new int[1];
		mGLId.glGenBuffers(1, name, 0);
		mBoxCoords = name[0];

		gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
		gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity()
				* (Float.SIZE / Byte.SIZE), xyBuffer, GL11.GL_STATIC_DRAW);

		gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
		gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);

		// Enable the texture coordinate array for Texture 1
		gl.glClientActiveTexture(GL11.GL_TEXTURE1);
		gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
		gl.glClientActiveTexture(GL11.GL_TEXTURE0);
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

		// mMatrixValues and mAlpha will be initialized in setSize()
	}

	@Override
	public void setSize(int width, int height) {
		Assert.assertTrue(width >= 0 && height >= 0);

		if (mTargetTexture == null) {
			mScreenWidth = width;
			mScreenHeight = height;
		}
		mAlpha = 1.0f;

		GL11 gl = mGL;
		gl.glViewport(0, 0, width, height);
		gl.glMatrixMode(GL11.GL_PROJECTION);
		gl.glLoadIdentity();
		GLU.gluOrtho2D(gl, 0, width, 0, height);

		gl.glMatrixMode(GL11.GL_MODELVIEW);
		gl.glLoadIdentity();

		float matrix[] = mMatrixValues;
		Matrix.setIdentityM(matrix, 0);
		// to match the graphic coordinate system in android, we flip it
		// vertically.
		if (mTargetTexture == null) {
			Matrix.translateM(matrix, 0, 0, height, 0);
			Matrix.scaleM(matrix, 0, 1, -1, 1);
		}
	}

	@Override
	public void setAlpha(float alpha) {
		Assert.assertTrue(alpha >= 0 && alpha <= 1);
		mAlpha = alpha;
	}

	@Override
	public float getAlpha() {
		return mAlpha;
	}

	@Override
	public void multiplyAlpha(float alpha) {
		Assert.assertTrue(alpha >= 0 && alpha <= 1);
		mAlpha *= alpha;
	}

	private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
		return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
	}

	@Override
	public void drawRect(float x, float y, float width, float height,
			GLPaint paint) {
		GL11 gl = mGL;

		mGLState.setColorMode(paint.getColor(), mAlpha);
		mGLState.setLineWidth(paint.getLineWidth());

		saveTransform();
		translate(x, y);
		scale(width, height, 1);

		gl.glLoadMatrixf(mMatrixValues, 0);
		gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);

		restoreTransform();
		mCountDrawLine++;
	}

	@Override
	public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
		GL11 gl = mGL;

		mGLState.setColorMode(paint.getColor(), mAlpha);
		mGLState.setLineWidth(paint.getLineWidth());

		saveTransform();
		translate(x1, y1);
		scale(x2 - x1, y2 - y1, 1);

		gl.glLoadMatrixf(mMatrixValues, 0);
		gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2);

		restoreTransform();
		mCountDrawLine++;
	}

	@Override
	public void fillRect(float x, float y, float width, float height, int color) {
		mGLState.setColorMode(color, mAlpha);
		GL11 gl = mGL;

		saveTransform();
		translate(x, y);
		scale(width, height, 1);

		gl.glLoadMatrixf(mMatrixValues, 0);
		gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);

		restoreTransform();
		mCountFillRect++;
	}

	@Override
	public void translate(float x, float y, float z) {
		Matrix.translateM(mMatrixValues, 0, x, y, z);
	}

	// This is a faster version of translate(x, y, z) because
	// (1) we knows z = 0, (2) we inline the Matrix.translateM call,
	// (3) we unroll the loop
	@Override
	public void translate(float x, float y) {
		float[] m = mMatrixValues;
		m[12] += m[0] * x + m[4] * y;
		m[13] += m[1] * x + m[5] * y;
		m[14] += m[2] * x + m[6] * y;
		m[15] += m[3] * x + m[7] * y;
	}

	@Override
	public void scale(float sx, float sy, float sz) {
		Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
	}

	@Override
	public void rotate(float angle, float x, float y, float z) {
		if (angle == 0)
			return;
		float[] temp = mTempMatrix;
		Matrix.setRotateM(temp, 0, angle, x, y, z);
		Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);
		System.arraycopy(temp, 16, mMatrixValues, 0, 16);
	}

	@Override
	public void multiplyMatrix(float matrix[], int offset) {
		float[] temp = mTempMatrix;
		Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
		System.arraycopy(temp, 0, mMatrixValues, 0, 16);
	}

	private void textureRect(float x, float y, float width, float height) {
		GL11 gl = mGL;

		saveTransform();
		translate(x, y);
		scale(width, height, 1);

		gl.glLoadMatrixf(mMatrixValues, 0);
		gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);

		restoreTransform();
		mCountTextureRect++;
	}

	@Override
	public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
			int uvBuffer, int indexBuffer, int indexCount) {
		float alpha = mAlpha;
		if (!bindTexture(tex))
			return;

		mGLState.setBlendEnabled(mBlendEnabled
				&& (!tex.isOpaque() || alpha < OPAQUE_ALPHA));
		mGLState.setTextureAlpha(alpha);

		// Reset the texture matrix. We will set our own texture coordinates
		// below.
		setTextureCoords(0, 0, 1, 1);

		saveTransform();
		translate(x, y);

		mGL.glLoadMatrixf(mMatrixValues, 0);

		mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer);
		mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);

		mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer);
		mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);

		mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
		mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP, indexCount,
				GL11.GL_UNSIGNED_BYTE, 0);

		mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
		mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
		mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);

		restoreTransform();
		mCountDrawMesh++;
	}

	// Transforms two points by the given matrix m. The result
	// {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.
	private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) {
		float[] r = mMapPointsBuffer;

		// Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.
		float x3 = m[0] * x1 + m[4] * y1 + m[12];
		float y3 = m[1] * x1 + m[5] * y1 + m[13];
		float w3 = m[3] * x1 + m[7] * y1 + m[15];
		r[0] = x3 / w3;
		r[1] = y3 / w3;

		// Same for x2 y2.
		float x4 = m[0] * x2 + m[4] * y2 + m[12];
		float y4 = m[1] * x2 + m[5] * y2 + m[13];
		float w4 = m[3] * x2 + m[7] * y2 + m[15];
		r[2] = x4 / w4;
		r[3] = y4 / w4;

		return r;
	}

	private void drawBoundTexture(BasicTexture texture, int x, int y,
			int width, int height) {
		// Test whether it has been rotated or flipped, if so, glDrawTexiOES
		// won't work
		if (isMatrixRotatedOrFlipped(mMatrixValues)) {
			if (texture.hasBorder()) {
				setTextureCoords(
						1.0f / texture.getTextureWidth(),
						1.0f / texture.getTextureHeight(),
						(texture.getWidth() - 1.0f) / texture.getTextureWidth(),
						(texture.getHeight() - 1.0f)
								/ texture.getTextureHeight());
			} else {
				setTextureCoords(
						0,
						0,
						(float) texture.getWidth() / texture.getTextureWidth(),
						(float) texture.getHeight()
								/ texture.getTextureHeight());
			}
			textureRect(x, y, width, height);
		} else {
			// draw the rect from bottom-left to top-right
			float points[] = mapPoints(mMatrixValues, x, y + height, x + width,
					y);
			x = (int) (points[0] + 0.5f);
			y = (int) (points[1] + 0.5f);
			width = (int) (points[2] + 0.5f) - x;
			height = (int) (points[3] + 0.5f) - y;
			if (width > 0 && height > 0) {
				((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);
				mCountTextureOES++;
			}
		}
	}

	@Override
	public void drawTexture(BasicTexture texture, int x, int y, int width,
			int height) {
		drawTexture(texture, x, y, width, height, mAlpha);
	}

	private void drawTexture(BasicTexture texture, int x, int y, int width,
			int height, float alpha) {
		if (width <= 0 || height <= 0)
			return;

		mGLState.setBlendEnabled(mBlendEnabled
				&& (!texture.isOpaque() || alpha < OPAQUE_ALPHA));
		if (!bindTexture(texture))
			return;
		mGLState.setTextureAlpha(alpha);
		drawBoundTexture(texture, x, y, width, height);
	}

	@Override
	public void drawTexture(BasicTexture texture, RectF source, RectF target) {
		if (target.width() <= 0 || target.height() <= 0)
			return;

		// Copy the input to avoid changing it.
		mDrawTextureSourceRect.set(source);
		mDrawTextureTargetRect.set(target);
		source = mDrawTextureSourceRect;
		target = mDrawTextureTargetRect;

		mGLState.setBlendEnabled(mBlendEnabled
				&& (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
		if (!bindTexture(texture))
			return;
		convertCoordinate(source, target, texture);
		setTextureCoords(source);
		mGLState.setTextureAlpha(mAlpha);
		textureRect(target.left, target.top, target.width(), target.height());
	}

	@Override
	public void drawTexture(BasicTexture texture, float[] mTextureTransform,
			int x, int y, int w, int h) {
		mGLState.setBlendEnabled(mBlendEnabled
				&& (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
		if (!bindTexture(texture))
			return;
		setTextureCoords(mTextureTransform);
		mGLState.setTextureAlpha(mAlpha);
		textureRect(x, y, w, h);
	}

	// This function changes the source coordinate to the texture coordinates.
	// It also clips the source and target coordinates if it is beyond the
	// bound of the texture.
	private static void convertCoordinate(RectF source, RectF target,
			BasicTexture texture) {

		int width = texture.getWidth();
		int height = texture.getHeight();
		int texWidth = texture.getTextureWidth();
		int texHeight = texture.getTextureHeight();
		// Convert to texture coordinates
		source.left /= texWidth;
		source.right /= texWidth;
		source.top /= texHeight;
		source.bottom /= texHeight;

		// Clip if the rendering range is beyond the bound of the texture.
		float xBound = (float) width / texWidth;
		if (source.right > xBound) {
			target.right = target.left + target.width()
					* (xBound - source.left) / source.width();
			source.right = xBound;
		}
		float yBound = (float) height / texHeight;
		if (source.bottom > yBound) {
			target.bottom = target.top + target.height()
					* (yBound - source.top) / source.height();
			source.bottom = yBound;
		}
	}

	@Override
	public void drawMixed(BasicTexture from, int toColor, float ratio, int x,
			int y, int w, int h) {
		drawMixed(from, toColor, ratio, x, y, w, h, mAlpha);
	}

	private boolean bindTexture(BasicTexture texture) {
		if (!texture.onBind(this))
			return false;
		int target = texture.getTarget();
		mGLState.setTextureTarget(target);
		mGL.glBindTexture(target, texture.getId());
		return true;
	}

	private void setTextureColor(float r, float g, float b, float alpha) {
		float[] color = mTextureColor;
		color[0] = r;
		color[1] = g;
		color[2] = b;
		color[3] = alpha;
	}

	private void setMixedColor(int toColor, float ratio, float alpha) {
		//
		// The formula we want:
		// alpha * ((1 - ratio) * from + ratio * to)
		//
		// The formula that GL supports is in the form of:
		// combo * from + (1 - combo) * to * scale
		//
		// So, we have combo = alpha * (1 - ratio)
		// and scale = alpha * ratio / (1 - combo)
		//
		float combo = alpha * (1 - ratio);
		float scale = alpha * ratio / (1 - combo);

		// Specify the interpolation factor via the alpha component of
		// GL_TEXTURE_ENV_COLORs.
		// RGB component are get from toColor and will used as SRC1
		float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
		setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
				((toColor >>> 8) & 0xff) * colorScale, (toColor & 0xff)
						* colorScale, combo);
		GL11 gl = mGL;
		gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR,
				mTextureColor, 0);

		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB,
				GL11.GL_INTERPOLATE);
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA,
				GL11.GL_INTERPOLATE);
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB,
				GL11.GL_SRC_COLOR);
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA,
				GL11.GL_SRC_ALPHA);

		// Wire up the interpolation factor for RGB.
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB,
				GL11.GL_SRC_ALPHA);

		// Wire up the interpolation factor for alpha.
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
		gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA,
				GL11.GL_SRC_ALPHA);

	}

	@Override
	public void drawMixed(BasicTexture from, int toColor, float ratio,
			RectF source, RectF target) {
		if (target.width() <= 0 || target.height() <= 0)
			return;

		if (ratio <= 0.01f) {
			drawTexture(from, source, target);
			return;
		} else if (ratio >= 1) {
			fillRect(target.left, target.top, target.width(), target.height(),
					toColor);
			return;
		}

		float alpha = mAlpha;

		// Copy the input to avoid changing it.
		mDrawTextureSourceRect.set(source);
		mDrawTextureTargetRect.set(target);
		source = mDrawTextureSourceRect;
		target = mDrawTextureTargetRect;

		mGLState.setBlendEnabled(mBlendEnabled
				&& (!from.isOpaque() || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));

		if (!bindTexture(from))
			return;

		// Interpolate the RGB and alpha values between both textures.
		mGLState.setTexEnvMode(GL11.GL_COMBINE);
		setMixedColor(toColor, ratio, alpha);
		convertCoordinate(source, target, from);
		setTextureCoords(source);
		textureRect(target.left, target.top, target.width(), target.height());
		mGLState.setTexEnvMode(GL11.GL_REPLACE);
	}

	private void drawMixed(BasicTexture from, int toColor, float ratio, int x,
			int y, int width, int height, float alpha) {
		// change from 0 to 0.01f to prevent getting divided by zero below
		if (ratio <= 0.01f) {
			drawTexture(from, x, y, width, height, alpha);
			return;
		} else if (ratio >= 1) {
			fillRect(x, y, width, height, toColor);
			return;
		}

		mGLState.setBlendEnabled(mBlendEnabled
				&& (!from.isOpaque() || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));

		final GL11 gl = mGL;
		if (!bindTexture(from))
			return;

		// Interpolate the RGB and alpha values between both textures.
		mGLState.setTexEnvMode(GL11.GL_COMBINE);
		setMixedColor(toColor, ratio, alpha);

		drawBoundTexture(from, x, y, width, height);
		mGLState.setTexEnvMode(GL11.GL_REPLACE);
	}

	// TODO: the code only work for 2D should get fixed for 3D or removed
	private static final int MSKEW_X = 4;
	private static final int MSKEW_Y = 1;
	private static final int MSCALE_X = 0;
	private static final int MSCALE_Y = 5;

	private static boolean isMatrixRotatedOrFlipped(float matrix[]) {
		final float eps = 1e-5f;
		return Math.abs(matrix[MSKEW_X]) > eps
				|| Math.abs(matrix[MSKEW_Y]) > eps || matrix[MSCALE_X] < -eps
				|| matrix[MSCALE_Y] > eps;
	}

	private static class GLState {

		private final GL11 mGL;

		private int mTexEnvMode = GL11.GL_REPLACE;
		private float mTextureAlpha = 1.0f;
		private int mTextureTarget = GL11.GL_TEXTURE_2D;
		private boolean mBlendEnabled = true;
		private float mLineWidth = 1.0f;
		private boolean mLineSmooth = false;

		public GLState(GL11 gl) {
			mGL = gl;

			// Disable unused state
			gl.glDisable(GL11.GL_LIGHTING);

			// Enable used features
			gl.glEnable(GL11.GL_DITHER);

			gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
			gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
			gl.glEnable(GL11.GL_TEXTURE_2D);

			gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE,
					GL11.GL_REPLACE);

			// Set the background color
			gl.glClearColor(0f, 0f, 0f, 0f);

			gl.glEnable(GL11.GL_BLEND);
			gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);

			// We use 565 or 8888 format, so set the alignment to 2 bytes/pixel.
			gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2);
		}

		public void setTexEnvMode(int mode) {
			if (mTexEnvMode == mode)
				return;
			mTexEnvMode = mode;
			mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode);
		}

		public void setLineWidth(float width) {
			if (mLineWidth == width)
				return;
			mLineWidth = width;
			mGL.glLineWidth(width);
		}

		public void setTextureAlpha(float alpha) {
			if (mTextureAlpha == alpha)
				return;
			mTextureAlpha = alpha;
			if (alpha >= OPAQUE_ALPHA) {
				// The alpha is need for those texture without alpha channel
				mGL.glColor4f(1, 1, 1, 1);
				setTexEnvMode(GL11.GL_REPLACE);
			} else {
				mGL.glColor4f(alpha, alpha, alpha, alpha);
				setTexEnvMode(GL11.GL_MODULATE);
			}
		}

		public void setColorMode(int color, float alpha) {
			setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA);

			// Set mTextureAlpha to an invalid value, so that it will reset
			// again in setTextureAlpha(float) later.
			mTextureAlpha = -1.0f;

			setTextureTarget(0);

			float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
			mGL.glColor4x(Math.round(((color >> 16) & 0xFF) * prealpha),
					Math.round(((color >> 8) & 0xFF) * prealpha),
					Math.round((color & 0xFF) * prealpha),
					Math.round(255 * prealpha));
		}

		// target is a value like GL_TEXTURE_2D. If target = 0, texturing is
		// disabled.
		public void setTextureTarget(int target) {
			if (mTextureTarget == target)
				return;
			if (mTextureTarget != 0) {
				mGL.glDisable(mTextureTarget);
			}
			mTextureTarget = target;
			if (mTextureTarget != 0) {
				mGL.glEnable(mTextureTarget);
			}
		}

		public void setBlendEnabled(boolean enabled) {
			if (mBlendEnabled == enabled)
				return;
			mBlendEnabled = enabled;
			if (enabled) {
				mGL.glEnable(GL11.GL_BLEND);
			} else {
				mGL.glDisable(GL11.GL_BLEND);
			}
		}
	}

	@Override
	public void clearBuffer(float[] argb) {
		if (argb != null && argb.length == 4) {
			mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]);
		} else {
			mGL.glClearColor(0, 0, 0, 1);
		}
		mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
	}

	@Override
	public void clearBuffer() {
		clearBuffer(null);
	}

	private void setTextureCoords(RectF source) {
		setTextureCoords(source.left, source.top, source.right, source.bottom);
	}

	private void setTextureCoords(float left, float top, float right,
			float bottom) {
		mGL.glMatrixMode(GL11.GL_TEXTURE);
		mTextureMatrixValues[0] = right - left;
		mTextureMatrixValues[5] = bottom - top;
		mTextureMatrixValues[10] = 1;
		mTextureMatrixValues[12] = left;
		mTextureMatrixValues[13] = top;
		mTextureMatrixValues[15] = 1;
		mGL.glLoadMatrixf(mTextureMatrixValues, 0);
		mGL.glMatrixMode(GL11.GL_MODELVIEW);
	}

	private void setTextureCoords(float[] mTextureTransform) {
		mGL.glMatrixMode(GL11.GL_TEXTURE);
		mGL.glLoadMatrixf(mTextureTransform, 0);
		mGL.glMatrixMode(GL11.GL_MODELVIEW);
	}

	// unloadTexture and deleteBuffer can be called from the finalizer thread,
	// so we synchronized on the mUnboundTextures object.
	@Override
	public boolean unloadTexture(BasicTexture t) {
		synchronized (mUnboundTextures) {
			if (!t.isLoaded())
				return false;
			mUnboundTextures.add(t.mId);
			return true;
		}
	}

	@Override
	public void deleteBuffer(int bufferId) {
		synchronized (mUnboundTextures) {
			mDeleteBuffers.add(bufferId);
		}
	}

	@Override
	public void deleteRecycledResources() {
		synchronized (mUnboundTextures) {
			IntArray ids = mUnboundTextures;
			if (ids.size() > 0) {
				mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(),
						0);
				ids.clear();
			}

			ids = mDeleteBuffers;
			if (ids.size() > 0) {
				mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(),
						0);
				ids.clear();
			}
		}
	}

	@Override
	public void save() {
		save(SAVE_FLAG_ALL);
	}

	@Override
	public void save(int saveFlags) {
		ConfigState config = obtainRestoreConfig();

		if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
			config.mAlpha = mAlpha;
		} else {
			config.mAlpha = -1;
		}

		if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
			System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
		} else {
			config.mMatrix[0] = Float.NEGATIVE_INFINITY;
		}

		mRestoreStack.add(config);
	}

	@Override
	public void restore() {
		if (mRestoreStack.isEmpty())
			throw new IllegalStateException();
		ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1);
		config.restore(this);
		freeRestoreConfig(config);
	}

	private void freeRestoreConfig(ConfigState action) {
		action.mNextFree = mRecycledRestoreAction;
		mRecycledRestoreAction = action;
	}

	private ConfigState obtainRestoreConfig() {
		if (mRecycledRestoreAction != null) {
			ConfigState result = mRecycledRestoreAction;
			mRecycledRestoreAction = result.mNextFree;
			return result;
		}
		return new ConfigState();
	}

	private static class ConfigState {
		float mAlpha;
		float mMatrix[] = new float[16];
		ConfigState mNextFree;

		public void restore(GLES11Canvas canvas) {
			if (mAlpha >= 0)
				canvas.setAlpha(mAlpha);
			if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
				System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
			}
		}
	}

	@Override
	public void dumpStatisticsAndClear() {
		String line = String.format(
				"MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d",
				mCountDrawMesh, mCountTextureRect, mCountTextureOES,
				mCountFillRect, mCountDrawLine);
		mCountDrawMesh = 0;
		mCountTextureRect = 0;
		mCountTextureOES = 0;
		mCountFillRect = 0;
		mCountDrawLine = 0;
		Log.d(TAG, line);
	}

	private void saveTransform() {
		System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16);
	}

	private void restoreTransform() {
		System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);
	}

	private void setRenderTarget(RawTexture texture) {
		GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL;

		if (mTargetTexture == null && texture != null) {
			mGLId.glGenBuffers(1, mFrameBuffer, 0);
			gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES,
					mFrameBuffer[0]);
		}
		if (mTargetTexture != null && texture == null) {
			gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0);
			gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0);
		}

		mTargetTexture = texture;
		if (texture == null) {
			setSize(mScreenWidth, mScreenHeight);
		} else {
			setSize(texture.getWidth(), texture.getHeight());

			if (!texture.isLoaded())
				texture.prepare(this);

			gl11ep.glFramebufferTexture2DOES(
					GL11ExtensionPack.GL_FRAMEBUFFER_OES,
					GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,
					GL11.GL_TEXTURE_2D, texture.getId(), 0);

			checkFramebufferStatus(gl11ep);
		}
	}

	@Override
	public void endRenderTarget() {
		RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1);
		setRenderTarget(texture);
		restore(); // restore matrix and alpha
	}

	@Override
	public void beginRenderTarget(RawTexture texture) {
		save(); // save matrix and alpha
		mTargetStack.add(mTargetTexture);
		setRenderTarget(texture);
	}

	private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) {
		int status = gl11ep
				.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES);
		if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) {
			String msg = "";
			switch (status) {
			case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
				msg = "FRAMEBUFFER_FORMATS";
				break;
			case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
				msg = "FRAMEBUFFER_ATTACHMENT";
				break;
			case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
				msg = "FRAMEBUFFER_MISSING_ATTACHMENT";
				break;
			case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES:
				msg = "FRAMEBUFFER_DRAW_BUFFER";
				break;
			case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES:
				msg = "FRAMEBUFFER_READ_BUFFER";
				break;
			case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES:
				msg = "FRAMEBUFFER_UNSUPPORTED";
				break;
			case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
				msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
				break;
			}
			throw new RuntimeException(msg + ":" + Integer.toHexString(status));
		}
	}

	@Override
	public void setTextureParameters(BasicTexture texture) {
		int width = texture.getWidth();
		int height = texture.getHeight();
		// Define a vertically flipped crop rectangle for OES_draw_texture.
		// The four values in sCropRect are: left, bottom, width, and
		// height. Negative value of width or height means flip.
		sCropRect[0] = 0;
		sCropRect[1] = height;
		sCropRect[2] = width;
		sCropRect[3] = -height;

		// Set texture parameters.
		int target = texture.getTarget();
		mGL.glBindTexture(target, texture.getId());
		mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES,
				sCropRect, 0);
		mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S,
				GL11.GL_CLAMP_TO_EDGE);
		mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T,
				GL11.GL_CLAMP_TO_EDGE);
		mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
		mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
	}

	@Override
	public void initializeTextureSize(BasicTexture texture, int format, int type) {
		int target = texture.getTarget();
		mGL.glBindTexture(target, texture.getId());
		int width = texture.getTextureWidth();
		int height = texture.getTextureHeight();
		mGL.glTexImage2D(target, 0, format, width, height, 0, format, type,
				null);
	}

	@Override
	public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
		int target = texture.getTarget();
		mGL.glBindTexture(target, texture.getId());
		GLUtils.texImage2D(target, 0, bitmap, 0);
	}

	@Override
	public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset,
			Bitmap bitmap, int format, int type) {
		int target = texture.getTarget();
		mGL.glBindTexture(target, texture.getId());
		GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
	}

	@Override
	public int uploadBuffer(FloatBuffer buf) {
		return uploadBuffer(buf, Float.SIZE / Byte.SIZE);
	}

	@Override
	public int uploadBuffer(ByteBuffer buf) {
		return uploadBuffer(buf, 1);
	}

	private int uploadBuffer(Buffer buf, int elementSize) {
		int[] bufferIds = new int[1];
		mGLId.glGenBuffers(bufferIds.length, bufferIds, 0);
		int bufferId = bufferIds[0];
		mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId);
		mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize,
				buf, GL11.GL_STATIC_DRAW);
		return bufferId;
	}

	@Override
	public void recoverFromLightCycle() {
		// This is only required for GLES20
	}

	@Override
	public void getBounds(Rect bounds, int x, int y, int width, int height) {
		// This is only required for GLES20
	}

	@Override
	public GLId getGLId() {
		return mGLId;
	}
}
